/* $Id: scrollbar.jquery.js 22 2009-07-09 10:08:21Z geraldb $

Unobtrusive Accessible jQuery Graphical Scrollbars.

Overview:
	jQuery.fn.scrollbar()
	Adds child nodes to selected nodes in order to simulate scrollable divs.
	Padding/margin on most elements (probably needs some testing).
	Scrolling via keyboard with up/down arrows and [/] keys for IE.
	# links inside scrollable content (does not work in safari).

Parameters:
	position: (String) position of scrollbar(s)
		Allowed: 'right', 'top', 'topBottom'
		Default: 'right'
	prepend: (String) Text to be prepended to classes on nodes.
		Default: 'scrollbar'
	clickScroll: (Float) Distance to scroll on click
		Default: 
	mouseScroll: (Float) Distance to scroll on mouse wheel scroll
		Default: 
	animation: (String/Float) Animation to apply to movement
		Default: 

Usage:
	HTML:
	<div id="scroller1">
		<!-- Content -->
	</div>

	JavaScript:
	$('#scroller1').scrollbar();

Author:
	gburns

TODO:
	Possibly adjust code to account for margins/etc
*/

(function($) {
$.fn.scrollbar = function(settings) {
	// Allowed option settings
	var options = {
		position: {
			'default': 'right',
			right: 'right',
			// left: 'left', // TODO: Support scrollbars on left side
			top: 'top',
			topBottom: 'top'
		}
	};

	if ( typeof(settings) != 'object' ) settings = { position: settings };

	settings = $.extend({
		position: options.position['default'],
		prepend: 'scrollbar',
		clickScroll: 19,
		mouseScroll: 58,
		animation: null
	}, settings || {});

	settings.position = options.position[settings.position] || options.position['default'];

	return this.each(function() {
		if ( this.clientHeight >= this.scrollHeight ) return;

		var $this = $(this),
			// Getting the padding to transfer to the content DIV we will make
			padding_top = (parseInt($this.css('paddingTop')) || 0),
			padding_right = (parseInt($this.css('paddingRight')) || 0),
			padding_bottom = (parseInt($this.css('paddingBottom')) || 0),
			padding_left = (parseInt($this.css('paddingLeft')) || 0);

		// Reset any # in the location
		$this[0].scrollTop = 0;

		$this
		.css({
			zoom: 1, // Force hasLayout for IE6 or width is reported incorrectly once
			overflow: 'hidden'
		});

		var $this_width = $this[0].clientWidth;
		var $this_height = $this[0].clientHeight;

		$this
		.addClass(settings.prepend + 'Main')
		.css({
			padding: 0,
			width: $this_width + 'px',
			height: $this_height + 'px'
		});

		// DIV containing our content from the original DIV
		var $content_node = $('<div/>')
			.addClass(settings.prepend + 'Content')
			.attr({innerHTML: $this.attr('innerHTML')})
			.css({
				position: 'absolute',
				paddingTop: padding_top + 'px',
				paddingRight: padding_right + 'px',
				paddingBottom: padding_bottom + 'px',
				paddingLeft: padding_left + 'px'
			});

		// DIV to contain the content DIV, used for scrolling
		var $container_node = $('<div/>')
			.addClass(settings.prepend + 'Container')
			.append($content_node)
			.css({
				overflow: 'hidden',
				position: 'relative',
				zoom: 1
			});

		// Scroll Up link for accessibility
		var $up_link = $('<a href="javascript:;">Scroll Up</a>')
			.css({
				overflow: 'hidden',
				textIndent: '-1000px',
				display: 'block'
			});

		// Scroll Up DIV for mouse click support
		var $up_node = $('<p/>')
			.addClass(settings.prepend + 'Up')
			.append($('<span/>')
				.css({display: 'block'})
				.append($('<span/>')
					.css({display: 'block'})
					.append($up_link)
				)
			)
			.css({
				position: 'absolute',
				zIndex: 2
			});

		// Scroll Down link for accessibility
		var $down_link = $('<a href="javascript:;">Scroll Down</a>')
			.css({
				overflow: 'hidden',
				textIndent: '-1000px',
				display: 'block'
			});

		// Scroll Down DIV for mouse click support
		var $down_node = $('<p/>')
			.addClass(settings.prepend + 'Down')
			.append($('<span/>')
				.css({display: 'block'})
				.append($('<span/>')
					.css({display: 'block'})
					.append($down_link)
				)
			)
			.css({
				position: 'absolute',
				zIndex: 2,
				bottom: 0
			});

		if ( settings.position != 'top' ) {
			// Scroll Bar for mouse drag support
			var $bar_link = $('<a href="javascript:;">Scroll ( [ / ] keys )</a>')
				.css({
					height: '100%',
					display: 'block',
					textIndent: '-1000px'
				})
				.bind('mousedown', function(event) {
					event.preventDefault();
				});

			var $bar_node = $('<div/>')
				.addClass(settings.prepend + 'Bar')
				.append($('<span/>')
					.append($bar_link)
					.css({
						height: '100%',
						display: 'block'
					})
				);

			var $track_node = $('<p/>')
				.addClass(settings.prepend + 'Track')
				.append($bar_node)
				.css({
					position: 'absolute',
					zIndex: 1
				});
		}

		// Empty original DIV and fill with elements created above
		$this
			.empty()
			.append($up_node);

		if ( settings.position != 'top' ) {
			$this
				.append($track_node);
		}

		$this
			.append($down_node)
			.append($container_node);

		// Resize and position elements
		var $this_pos = {x: 0, y: 0};
		var obj = this;
		if ( obj.offsetParent ) {
			$this_pos.x = obj.offsetLeft;
			$this_pos.y = obj.offsetTop;
			while ( obj = obj.offsetParent ) {
				if ( $(obj).css('position') == 'absolute' ) {
					break;
				}
				$this_pos.x += obj.offsetLeft
				$this_pos.y += obj.offsetTop
			}
		} else {
			$this_pos.x = obj.offsetLeft;
			$this_pos.y = obj.offsetTop;
		}

		var content_width;
		var container_css;
		var up_css;
		var down_css;

		if ( settings.position != 'top' ) {
			content_width = $this_width - $up_node.outerWidth() - padding_left - padding_right + 'px';

			$bar_node
				.css({
					position: 'absolute',
					top: $up_node.height() + 'px'
				});

			container_css = {
				width: $this_width - $up_node.outerWidth() + 'px',
				height: $this_height + 'px'
			}

			up_css = {
				top: $this_pos.y + (parseInt($this.css('borderTopWidth')) || 0) + 'px',
				left: $this_pos.x + $this.innerWidth() + (parseInt($this.css('borderLeftWidth')) || 0) - $up_node.outerWidth() + 'px'
			}

			down_css = {
				top: $this_pos.y + $this.innerHeight() + (parseInt($this.css('borderTopWidth')) || 0) - $down_node.outerWidth() + 'px',
				left: $this_pos.x + $this.innerWidth() + (parseInt($this.css('borderLeftWidth')) || 0) - $down_node.outerWidth() + 'px'
			}

			$bar_node
				.css({
					height: parseInt(($this.outerHeight() / $content_node.outerHeight()) * ($this.outerHeight() - $down_node.outerHeight() - $up_node.outerHeight())) + 'px',
					top: 0
				});

			$bar_link
				.css({
					height: parseInt(($this.outerHeight() / $content_node.outerHeight()) * ($this.outerHeight() - $down_node.outerHeight() - $up_node.outerHeight())) + 'px'
				});

			$track_node
				.css({
					top: $this_pos.y + (parseInt($this.css('borderTopWidth')) || 0) + $up_node.outerHeight() + 'px',
					left: $this_pos.x + $this.innerWidth() + (parseInt($this.css('borderLeftWidth')) || 0) - $up_node.outerWidth() + 'px',
					height: $this.innerHeight() - $down_node.outerHeight() - $up_node.outerHeight() - ($track_node.outerHeight() - $track_node.innerHeight()) + 'px'
				});
		} else {
			content_width = $this_width - padding_left - padding_right + 'px';

			container_css = {
				width: $this_width + 'px',
				height: $this_height - $up_node.outerHeight() - $down_node.outerHeight() + 'px',
				top: $up_node.outerHeight() + 'px'
			}

			up_css = {
				top: $this_pos.y + (parseInt($this.css('borderTopWidth')) || 0) + 'px',
				left: $this_pos.x + (parseInt($this.css('borderLeftWidth')) || 0) + 'px',
				width: $this.innerWidth() + 'px'
			}

			down_css = {
				top: $this_pos.y + $this.innerHeight() + (parseInt($this.css('borderTopWidth')) || 0) - $down_node.outerWidth() + 'px',
				left: $this_pos.x + (parseInt($this.css('borderLeftWidth')) || 0) + 'px',
				width: $this.innerWidth() + 'px'
			}
		}

		$content_node
			.css({
				width: content_width
			});

		$container_node.css(container_css);

		$up_node.css(up_css);

		$down_node.css(down_css);

		// Trap # links
		$('*[id]', $content_node).each(function() {
			var target = this;
			$("a[href='#" + target.id + "']").click(function() {
				var target_pos = {x: 0, y: 0};
				var obj = target;
				target_pos.x = obj.offsetLeft;
				target_pos.y = obj.offsetTop;

				if ( obj.offsetParent ) {
					while ( obj = obj.offsetParent ) {
						if ( $(obj).css('position') == 'absolute' ) {
							break;
						}
						target_pos.x += obj.offsetLeft
						target_pos.y += obj.offsetTop
					}
				}

				content_scroll_vertical(target_pos.y, settings.animation);
				return false;
			});
		});

		var scroll_top_old = 0;
		// Bar scroll handler
		var content_scroll_vertical = function(try_top, animation) {
			var bar_top
			$container_node.stop();
			if ( settings.position != 'top' ) $bar_node.stop();
			if ( try_top < 0 ) try_top = 0;
			if ( try_top > $content_node.outerHeight() - $container_node.outerHeight() ) try_top = $content_node.outerHeight() - $container_node.outerHeight();
			if ( settings.position != 'top' ) bar_top = parseInt(try_top/($content_node.outerHeight() - $this.innerHeight()) * ($track_node[0].clientHeight - $bar_node.outerHeight())) + 'px'

			scroll_top_old = try_top;

			if ( $container_node[0].scrollTop == try_top ) return false;

			if ( animation ) {
				if ( settings.position != 'top' ) $bar_node.animate({top: bar_top}, animation);
				$container_node.animate({scrollTop: try_top}, animation);
			} else {
				if ( settings.position != 'top' ) $bar_node.css({top: bar_top});
				$container_node[0].scrollTop = try_top;
			}
			return true;
		};

		// Arrows/Scrollbar Mouse/Keyboard events
		var mouse_down = false;
		var scroll_timeout = null;

		var click_up = function(animation) {
			var try_top = $container_node[0].scrollTop - settings.clickScroll;
			content_scroll_vertical(try_top, animation);
			if ( mouse_down ) scroll_timeout = setTimeout(click_up, 50);
		};

		var click_down = function(animation) {
			var try_top = $container_node[0].scrollTop + settings.clickScroll;
			content_scroll_vertical(try_top, animation);
			if ( mouse_down ) scroll_timeout = setTimeout(click_down, 50);
		};

		$up_link
			.keypress(function(event) {
			// Enter || Up Arrow || Space Bar
				if ( event.which == 13 || event.which == 32 ) {
					click_up(settings.animation);
					return false;
				}
			})
			.click(function() {
				return false
			});

		$up_node
			.mousedown(function() {
				$up_node.addClass('mousedown');
				click_up(settings.animation);
				mouse_down = true;
				scroll_timeout = setTimeout(click_up, 500);
			})
			.mouseup(function() {
				$up_node.removeClass('mousedown');
				mouse_down = false;
				clearTimeout(scroll_timeout);
				scroll_timeout = null;
			});

		$down_link
			.keypress(function(event) {
				// Enter || Space Bar
				if ( event.which == 13 || event.which == 32 ) {
					click_down(settings.animation);
					return false;
				}
			})
			.click(function() {
				return false
			});

		$down_node
			.mousedown(function() {
				$down_node.addClass('mousedown');
				click_down(settings.animation);
				mouse_down = true;
				scroll_timeout = setTimeout(click_down, 500);
			})
			.mouseup(function() {
				$down_node.removeClass('mousedown');
				mouse_down = false;
				clearTimeout(scroll_timeout);
				scroll_timeout = null;
			});

		$this
			.keypress(function(event) {
				if ( event.which == 40 || event.which == 93 ) {
					click_down();
					return false;
				} else if ( event.which == 38 || event.which == 91 ) {
					click_up();
					return false;
				}
			});

		// Mousewheel events if supported (http://$.com/plugins/project/mousewheel)
		if ( $.fn.mousewheel ) {
			var mousewheel_up = function() {
				var try_top = $container_node[0].scrollTop - settings.mouseScroll;
				return content_scroll_vertical(try_top);
			};

			var mousewheel_down = function() {
				var try_top = $container_node[0].scrollTop + settings.mouseScroll;
				return content_scroll_vertical(try_top);
			};

			$this
				.mousewheel(function(event, delta) {
					var retval = false;
					if ( delta < 0 ) {
						retval = mousewheel_down();
					} else if ( delta > 0 ) {
						retval = mousewheel_up();
					}
					return !retval;
				});
		}

		if ( settings.position != 'top' ) {
			// Draggable bar
			var mouse_start = null;
			var bar_start = null;

			var bar_drag = function(event) {
				content_scroll_vertical(($content_node.outerHeight() - $this.innerHeight()) * ((bar_start + (event.pageY - mouse_start)) / ($track_node[0].clientHeight - $bar_node.outerHeight())));
			}

			var return_false = function() {
				return false;
			}

			$bar_node
				.mousedown(function(event) {
					mouse_start = event.pageY;
					bar_start = parseInt($bar_node.css('top'));

					var $body = $(document.body);
					var old_moz_user_select = $body.css('MozUserSelect');
					$body.css({MozUserSelect: 'none'});

					$body
						.mousemove(bar_drag)
						.mouseup(function() {
							$body.css({MozUserSelect: ''});

							$body
								.unbind('mousemove', bar_drag);

							if ($.browser.msie) {
								$body
									.unbind('dragstart', return_false)
									.unbind('selectstart', return_false);
							}
						});

					if ($.browser.msie) {
						$body
							.bind('dragstart', return_false)
							.bind('selectstart', return_false);
					}
				});
		}
	});
}
})(jQuery);